<!DOCTYPE html>
<html lang="zh_cn">
<head>
          <title>来玩魔王的咚</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta charset="utf-8" />
        <!-- twitter card metadata -->
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="/images/mowang.png">
<meta name="twitter:site" content="">
<meta name="twitter:title" content="pytest（八）补充内容">
<meta name="twitter:description" content="<p>pytest系列的补充内容。使用临时目录；自定义失败断言的消息；关闭磁盘缓存pyc；raise重写和禁用；解决子目录中模块重名的问题。</p>">
        <!-- OG Tags -->
<meta property="og:url" content="/python-pytest-supplement.html"/>
<meta property="og:title" content="来玩魔王的咚 | pytest（八）补充内容" />
<meta property="og:description" content="<p>pytest系列的补充内容。使用临时目录；自定义失败断言的消息；关闭磁盘缓存pyc；raise重写和禁用；解决子目录中模块重名的问题。</p>" />
        <!-- favicon -->
        <link rel="icon" type="image/png" href="/images/mowang.png">
        <!-- moment.js for date formatting -->
        <script src="/theme/js/moment.js"></script>
        <!-- css -->
        <link rel="stylesheet" type="text/css" href="/theme/css/main.css" />
        <!-- 左边的menu，如果页面高度不够，就跟着滚动，否则文章分类显示不全 -->
        <link rel="stylesheet" type="text/css" href="/theme/css/mycss/menu.css" />
		<script>
			
                /*! grunt-grunticon Stylesheet Loader - v2.1.2 | https://github.com/filamentgroup/grunticon | (c) 2015 Scott Jehl, Filament Group, Inc. | MIT license. */
    
    (function(e){function t(t,n,r,o){"use strict";function a(){for(var e,n=0;u.length>n;n++)u[n].href&&u[n].href.indexOf(t)>-1&&(e=!0);e?i.media=r||"all":setTimeout(a)}var i=e.document.createElement("link"),l=n||e.document.getElementsByTagName("script")[0],u=e.document.styleSheets;return i.rel="stylesheet",i.href=t,i.media="only x",i.onload=o||null,l.parentNode.insertBefore(i,l),a(),i}var n=function(r,o){"use strict";if(r&&3===r.length){var a=e.navigator,i=e.Image,l=!(!document.createElementNS||!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect||!document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image","1.1")||e.opera&&-1===a.userAgent.indexOf("Chrome")||-1!==a.userAgent.indexOf("Series40")),u=new i;u.onerror=function(){n.method="png",n.href=r[2],t(r[2])},u.onload=function(){var e=1===u.width&&1===u.height,a=r[e&&l?0:e?1:2];n.method=e&&l?"svg":e?"datapng":"png",n.href=a,t(a,null,null,o)},u.src="",document.documentElement.className+=" grunticon"}};n.loadCSS=t,e.grunticon=n})(this);(function(e,t){"use strict";var n=t.document,r="grunticon:",o=function(e){if(n.attachEvent?"complete"===n.readyState:"loading"!==n.readyState)e();else{var t=!1;n.addEventListener("readystatechange",function(){t||(t=!0,e())},!1)}},a=function(e){return t.document.querySelector('link[href$="'+e+'"]')},c=function(e){var t,n,o,a,c,i,u={};if(t=e.sheet,!t)return u;n=t.cssRules?t.cssRules:t.rules;for(var l=0;n.length>l;l++)o=n[l].cssText,a=r+n[l].selectorText,c=o.split(");")[0].match(/US\-ASCII\,([^"']+)/),c&&c[1]&&(i=decodeURIComponent(c[1]),u[a]=i);return u},i=function(e){var t,o,a;o="data-grunticon-embed";for(var c in e)if(a=c.slice(r.length),t=n.querySelectorAll(a+"["+o+"]"),t.length)for(var i=0;t.length>i;i++)t[i].innerHTML=e[c],t[i].style.backgroundImage="none",t[i].removeAttribute(o);return t},u=function(t){"svg"===e.method&&o(function(){i(c(a(e.href))),"function"==typeof t&&t()})};e.embedIcons=i,e.getCSS=a,e.getIcons=c,e.ready=o,e.svgLoadedCallback=u,e.embedSVG=u})(grunticon,this);
                
                grunticon(["/theme/css/icons.data.svg.css", "/theme/css/icons.data.png.css", "/theme/css/icons.fallback.css"]);
            </script>
        <noscript><link href="/theme/css/icons.fallback.css" rel="stylesheet"></noscript>
        <!-- menu toggle javascript -->
        <script type="text/javascript">
            document.addEventListener("DOMContentLoaded", initMenu);
            
            function initMenu(){
                var menu = document.getElementById("menu");
                var menulink = document.getElementById("menu-link");
                menulink.addEventListener("click", function toggleMenu(){
                        window.event.preventDefault();
                        menulink.classList.toggle('active');
                        menu.classList.toggle('active');              
                    });
            };
        </script>
        <!-- 不蒜子 -->
        <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>

    <meta name="description" content="<p>pytest系列的补充内容。使用临时目录；自定义失败断言的消息；关闭磁盘缓存pyc；raise重写和禁用；解决子目录中模块重名的问题。</p>" />

    <meta name="tags" content="测试" />
  <!-- 替换部分base的样式，看文章时，要再宽一点，右边有很多空间可以撑开 -->
  <link rel="stylesheet" type="text/css" href="/theme/css/mycss/article.css" />

</head>
<body>
    <div role="banner" id="masthead">
        <header>
            <a href="/"><img src="/images/mowang.png" alt="McManus Logo"></a>
                <h1>来玩魔王的咚@骑士救兵</h1>
            <a href="#menu" id="menu-link">more stuff</a>
            <nav id="menu">
                <ul>
                        <li><a href="/tags">tags</a></li>
                            <li><a href="/category/cloud.html">Cloud</a></li>
                            <li><a href="/category/docker.html">Docker</a></li>
                            <li><a href="/category/go.html">Go</a></li>
                            <li><a href="/category/linux.html">Linux</a></li>
                            <li class="active"><a href="/category/python.html">Python</a></li>
                            <li><a href="/category/xue-xi-bi-ji.html">学习笔记</a></li>
                            <li><a href="/category/yun-wei-zi-dong-hua.html">运维自动化</a></li>
                </ul>
            </nav>
        </header>
    </div>
        <div class="page" role="main">
  <div class="article" role="article">
    <article>
        <footer>
            <a name="top"></a>
            <p>
              <time datetime=" 2022-04-03 15:40:00+08:00">
                <script>document.write(moment('2022-04-03 15:40:00+08:00').format('LL'));</script>
              </time>
              ~
              <time datetime=" 2022-04-03 15:40:00+08:00">
                <script>document.write(moment('2022-04-04 20:50:00+08:00').format('LL'));</script>
              </time>
            </p>
        </footer>
        <header>
          <h2>
            pytest（八）补充内容
          </h2>
        </header>
      <div class="content">
         <div class="toc">
<ul>
<li><a href="#shi-yong-lin-shi-mu-lu-tmp_path">使用临时目录 tmp_path</a><ul>
<li><a href="#tmp_path_factory">tmp_path_factory</a></li>
<li><a href="#tmpdir-he-tmpdir_factory">tmpdir 和 tmpdir_factory</a></li>
</ul>
</li>
<li><a href="#zi-ding-yi-shi-bai-xiao-xi">自定义失败消息</a></li>
<li><a href="#guan-bi-ci-pan-huan-cun-pyc">关闭磁盘缓存pyc</a></li>
<li><a href="#raise-zhong-xie">raise 重写</a><ul>
<li><a href="#jin-yong-zhong-xie">禁用重写</a></li>
</ul>
</li>
<li><a href="#zi-mu-lu-zhong-mo-kuai-wen-jian-zhong-ming">子目录中模块（文件）重名</a></li>
</ul>
</div>
<h3 id="shi-yong-lin-shi-mu-lu-tmp_path"><a class="toclink" href="#shi-yong-lin-shi-mu-lu-tmp_path">使用临时目录 tmp_path</a></h3>
<p>pytest提供了内置的固件，可以为测试创建一个临时的目录使用。直接将参数名tmp_path传给测试函数即可，拿到的是一个<code>pathlib.Path</code>类型：</p>
<div class="highlight"><pre><span></span><code><span class="n">CONTENT</span> <span class="o">=</span> <span class="s2">&quot;content&quot;</span>

<span class="k">def</span> <span class="nf">test_create_file</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">):</span>
    <span class="n">d</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&quot;sub&quot;</span>
    <span class="n">d</span><span class="o">.</span><span class="n">mkdir</span><span class="p">()</span>
    <span class="n">p</span> <span class="o">=</span> <span class="n">d</span> <span class="o">/</span> <span class="s2">&quot;hello.txt&quot;</span>
    <span class="n">p</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">CONTENT</span><span class="p">)</span>
    <span class="k">assert</span> <span class="n">p</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span> <span class="o">==</span> <span class="n">CONTENT</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">tmp_path</span><span class="o">.</span><span class="n">iterdir</span><span class="p">()))</span> <span class="o">==</span> <span class="mi">1</span>
    <span class="k">assert</span> <span class="mi">0</span>

<span class="sd">&#39;&#39;&#39;</span>
<span class="sd">assert 0</span>
<span class="sd">tmp_path = WindowsPath(&#39;C:/Users/Steed/AppData/Local/Temp/pytest-of-Steed/pytest-18/test_create_file0&#39;)</span>

<span class="sd">    def test_create_file(tmp_path):</span>
<span class="sd">        d = tmp_path / &quot;sub&quot;</span>
<span class="sd">        d.mkdir()</span>
<span class="sd">        p = d / &quot;hello.txt&quot;</span>
<span class="sd">        p.write_text(CONTENT)</span>
<span class="sd">        assert p.read_text() == CONTENT</span>
<span class="sd">        assert len(list(tmp_path.iterdir())) == 1</span>
<span class="sd">&gt;       assert 0</span>
<span class="sd">E       assert 0</span>
<span class="sd">&#39;&#39;&#39;</span>
</code></pre></div>

<p>最后的<code>assert 0</code>是一个验证效果常用的做法。如果测试成功是没有详细报告的，这里在最后加了一个使测试失败的断言，这样可以查看测试报告。  </p>
<p><code>tmp_path</code>会为每个测试创建一个独立的临时目录。这些创建的临时目录只会在系统中保留最近的3个。  </p>
<h4 id="tmp_path_factory"><a class="toclink" href="#tmp_path_factory">tmp_path_factory</a></h4>
<p>帮助说明如下：</p>
<div class="highlight"><pre><span></span><code>tmp_path_factory [session scope] -- .venv\lib\site-packages\_pytest\tmpdir.py:183
    Return a :class:`pytest.TempPathFactory` instance for the test session.
</code></pre></div>

<p>这是一个session级别的固件，并且不直接提供目录，而是提供一个创建临时目录的工厂类<code>pytest.TempPathFactory</code>。可以调用mktemp方法生成目录。mktemp方法返回的也是<code>pathlib.Path</code>类型。<br>
下面是使用的示例：</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pytest</span>

<span class="c1"># tmp_path_factory 是session范围，构造的image_file固件也设置为session范围</span>
<span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">&quot;session&quot;</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">image_file</span><span class="p">(</span><span class="n">tmp_path_factory</span><span class="p">):</span>
    <span class="n">img</span> <span class="o">=</span> <span class="n">compute_expensive_image</span><span class="p">()</span>
    <span class="c1"># 调用工厂类的mktemp方法创建目录，并且返回的是 pathlib.Path</span>
    <span class="n">fn</span> <span class="o">=</span> <span class="n">tmp_path_factory</span><span class="o">.</span><span class="n">mktemp</span><span class="p">(</span><span class="s2">&quot;data&quot;</span><span class="p">)</span> <span class="o">/</span> <span class="s2">&quot;img.png&quot;</span>
    <span class="n">img</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">fn</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">fn</span>

<span class="k">def</span> <span class="nf">test_histogram</span><span class="p">(</span><span class="n">image_file</span><span class="p">):</span>
    <span class="n">img</span> <span class="o">=</span> <span class="n">load_image</span><span class="p">(</span><span class="n">image_file</span><span class="p">)</span>
    <span class="c1"># compute and test histogram</span>
</code></pre></div>

<h4 id="tmpdir-he-tmpdir_factory"><a class="toclink" href="#tmpdir-he-tmpdir_factory">tmpdir 和 tmpdir_factory</a></h4>
<p>这两个固件不建议使用，也不展开了，对应tmp_path和tmp_path_factory完全可以替代。功能、效果基本都是一样的，差别在返回的对象是是<code>py.path.local</code>。<br>
推荐文件路径还是用新的<code>pathlib.Path</code>，所以固件也推荐tmp_path和tmp_path_factory。  </p>
<h3 id="zi-ding-yi-shi-bai-xiao-xi"><a class="toclink" href="#zi-ding-yi-shi-bai-xiao-xi">自定义失败消息</a></h3>
<p>比如在比较两个对象的时候，默认的输出可能是输出对象的内存地址，很不友好。这个时候就可以定义输出内容的格式。<br>
需要在一个特定的文件<code>conftest.py</code>，中定义一个<code>pytest_assertrepr_compare</code>的钩子，就是写一个函数。在比较指定的类型并且使用指定的比较方法的时候，返回你定义的消息。其他情况下还是默认的消息。  </p>
<p>下面是测试的示例：</p>
<div class="highlight"><pre><span></span><code><span class="c1"># content of test_foocompare.py</span>
<span class="k">class</span> <span class="nc">Foo</span><span class="p">:</span>
    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">val</span><span class="p">):</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">val</span> <span class="o">=</span> <span class="n">val</span>

    <span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">val</span> <span class="o">==</span> <span class="n">other</span><span class="o">.</span><span class="n">val</span>

<span class="k">def</span> <span class="nf">test_compare</span><span class="p">():</span>
    <span class="n">f1</span> <span class="o">=</span> <span class="n">Foo</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">f2</span> <span class="o">=</span> <span class="n">Foo</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="k">assert</span> <span class="n">f1</span> <span class="o">==</span> <span class="n">f2</span>

<span class="sd">&#39;&#39;&#39;</span>
<span class="sd">默认的断言很不友好</span>
<span class="sd">assert &lt;advanced.test_foocompare.Foo object at 0x0000027E071D9BE0&gt; == &lt;advanced.test_foocompare.Foo object at 0x0000027E071D9B50&gt;</span>
<span class="sd">&#39;&#39;&#39;</span>
</code></pre></div>

<p>这个情况也不能通过为Foo类定制__str__方法来解决。要定制__repr__方法才行，不过这个方法也不是专门为测试服务的。pytest提供了解决方案。  </p>
<p>需要在测试配置文件中定义pytest_assertrepr_compare函数：</p>
<div class="highlight"><pre><span></span><code><span class="c1"># content of conftest.py</span>
<span class="kn">from</span> <span class="nn">.test_foocompare</span> <span class="kn">import</span> <span class="n">Foo</span>

<span class="k">def</span> <span class="nf">pytest_assertrepr_compare</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">):</span>
    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">left</span><span class="p">,</span> <span class="n">Foo</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">right</span><span class="p">,</span> <span class="n">Foo</span><span class="p">)</span> <span class="ow">and</span> <span class="n">op</span> <span class="o">==</span> <span class="s2">&quot;==&quot;</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="s2">&quot;Comparing Foo instances: vals: </span><span class="si">{}</span><span class="s2"> != </span><span class="si">{}</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">left</span><span class="o">.</span><span class="n">val</span><span class="p">,</span> <span class="n">right</span><span class="o">.</span><span class="n">val</span><span class="p">),</span>
        <span class="p">]</span>

<span class="sd">&#39;&#39;&#39;</span>
<span class="sd">断言返回自定制的友好信息</span>
<span class="sd">assert Comparing Foo instances: vals: 1 != 2</span>
<span class="sd">&#39;&#39;&#39;</span>
</code></pre></div>

<p>这里的pytest_assertrepr_compare函数有固定的格式，参数是断言两边的类型和比较的符号。返回字符串的列表，列表的内容会分多行输出。  </p>
<h3 id="guan-bi-ci-pan-huan-cun-pyc"><a class="toclink" href="#guan-bi-ci-pan-huan-cun-pyc">关闭磁盘缓存pyc</a></h3>
<p>默认行为是将重写后的模块写到磁盘上以进行缓存，会生成<code>__pycache__</code>文件夹，里面都是<code>.pyc</code>文件。<br>
将下面的语句添加到<code>.py</code>文件的顶部，可以禁用这种行为:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">sys</span>

<span class="n">sys</span><span class="o">.</span><span class="n">dont_write_bytecode</span> <span class="o">=</span> <span class="kc">True</span>
</code></pre></div>

<p>另外，如果无写入权限，也会跳过缓存的步骤。比如，当前位置只读，或者在一个zipfile里。  </p>
<p>实测没什么效果，可能是因为还用了插件。  </p>
<h3 id="raise-zhong-xie"><a class="toclink" href="#raise-zhong-xie">raise 重写</a></h3>
<p>报告的失败消息是在运行assert语句之前重写它来实现的。重写的assert将消息放入断言失败的消息中。pytest只会重写自动发现收集到的那些assert，其他位置的assert不会被重写。  </p>
<h4 id="jin-yong-zhong-xie"><a class="toclink" href="#jin-yong-zhong-xie">禁用重写</a></h4>
<p>两种方式禁用:
- 模块级别禁用，将这个特定字符串<code>PYTEST_DONT_REWRITE</code>添加到python文件头部的doc信息中，只要能匹配到这串就生效，doc中还可以有其他内容。这个方法对插件有效。<br>
- 整体禁用，在参数中添加<code>--assert=plain</code>来禁用。这次pytest调用中有这个选项，那么就都不会重写了。  </p>
<p>效果就是不对输出消息做重写了，所以就是无输出了。因为一般是不写assert的第二个消息的，并且如果自己写了，pytest不会覆盖而是输出你写的消息。  </p>
<h3 id="zi-mu-lu-zhong-mo-kuai-wen-jian-zhong-ming"><a class="toclink" href="#zi-mu-lu-zhong-mo-kuai-wen-jian-zhong-ming">子目录中模块（文件）重名</a></h3>
<p>如果所有测试文件都在一个目录下，不会有这个问题。但是如果测试比较多，是可以创建子目录来对测试分类的。比如项目有多个产品，为每个产品创建一个子目录。<br>
不同的子目录下的文件是可以同名的，但是在pytest测试发现的时候可能会报错。原因是测试模块名重了。<br>
不改名的解决方法是在目录下创建<code>__init__.py</code>文件，让它变成一个包。  </p>
      </div>
      <div class="back-to-top">
        <a href="/">HOME</a>
        <a href="#top">TOP</a>
      </div>
    </article>
  </div>
<!-- end article -->
<!-- 页面往下滚动一段之后才会显示TOC -->
<script>
  window.onscroll = function() {
    var tocbox = document.getElementsByClassName('toc')[0];
    var osTop = document.documentElement.scrollTop || document.body.scrollTop;
    var osWidth = document.documentElement.scrollWidth || document.body.scrollWidth;
    // console.log(osTop)
    if (osTop>300 && osWidth>865) {
      tocbox.style.display = "block"
    }
    if (osTop<300 || osWidth<865) {
      tocbox.style.display = "none"
    }
  }
</script>
                <footer>
                    <div class="icons">
                    </div>
                    <span id="busuanzi_container_page_pv" style="padding: 10px">本文阅读量<span id="busuanzi_value_page_pv"></span>次</span>
                    <span id="busuanzi_container_site_pv" style="padding: 10px">本站总访问量<span id="busuanzi_value_site_pv"></span>次</span>
                    <span id="busuanzi_container_site_uv" style="padding: 10px">本站总访客数<span id="busuanzi_value_site_uv"></span>人</span>
                    <p>© <script>document.write(moment().format('YYYY'));</script> 749B</p>
                </footer>
        </div>
</body>
</html>